/*******************************************************************************
 * Copyright (c) 2010, 2023 fortiss, TU Wien ACIN, OFFIS e.V. and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Alois Zoitl - initial implementation, rework communication infrastructure and bug fixes
 *    Ingo Hegny - changed datatype for IP-communication
 *    Martin Melik-Merkumians -  fix typo in forte::com_infra::EComConnectionState, serial communication support for
 *WIN32 Michael Hofmann - fix for fragmented packets Patrik Smejkal - rename interrupt in interruptCCommFB Jörg Walter -
 *improve UDP multicast support
 *******************************************************************************/
#include "ipcomlayer.h"

#include "forte/cominfra/comlayersmanager.h"
#include "forte/util/devlog.h"
#include "forte/cominfra/basecommfb.h"
#include "forte/arch/forte_thread.h"
#include "forte/util/string_utils.h"

using namespace forte::literals;

namespace forte::com_infra {
  namespace {
    [[maybe_unused]] const ComLayerManager::EntryImpl<CIPComLayer> entry("ip"_STRID);
  }

  CIPComLayer::CIPComLayer(CComLayer *paUpperLayer, CBaseCommFB *paComFB) :
      CComLayer(paUpperLayer, paComFB),
      mSocketID(arch::CIPComSocketHandler::scmInvalidSocketDescriptor),
      mListeningID(arch::CIPComSocketHandler::scmInvalidSocketDescriptor),
      mInterruptResp(e_Nothing),
      mBufFillSize(0) {
    memset(
        mRecvBuffer, 0,
        sizeof(mRecvBuffer)); // TODO change this to  mRecvBuffer{0} in the extended list when fully switching to C++11
    memset(&mDestAddr, 0, sizeof(mDestAddr));
  }

  CIPComLayer::~CIPComLayer() = default;

  EComResponse CIPComLayer::sendData(void *paData, unsigned int paSize) {
    EComResponse eRetVal = e_ProcessDataOk;

    if ((nullptr != mFb) && (arch::CIPComSocketHandler::scmInvalidSocketDescriptor != mSocketID)) {
      switch (mFb->getComServiceType()) {
        case e_Server:
          if (0 >= arch::CIPComSocketHandler::sendDataOnTCP(mSocketID, static_cast<char *>(paData), paSize)) {
            closeSocket(&mSocketID);
            mConnectionState = e_Listening;
            eRetVal = e_InitTerminated;
          }
          break;
        case e_Client:
          if (0 >= arch::CIPComSocketHandler::sendDataOnTCP(mSocketID, static_cast<char *>(paData), paSize)) {
            eRetVal = e_InitTerminated;
          }
          break;
        case e_Publisher:
          if (0 >=
              arch::CIPComSocketHandler::sendDataOnUDP(mSocketID, &mDestAddr, static_cast<char *>(paData), paSize)) {
            eRetVal = e_InitTerminated;
          }
          break;
        case e_Subscriber:
          // do nothing as subscribers do not send data
          break;
      }
    }
    return eRetVal;
  }

  EComResponse CIPComLayer::processInterrupt() {
    if (e_ProcessDataOk == mInterruptResp) {
      switch (mConnectionState) {
        case e_Connected:
          if ((0 < mBufFillSize) && (nullptr != mTopLayer)) {
            util::CCriticalRegion criticalRegion(mFb->getFBLock());
            mInterruptResp = mTopLayer->recvData(mRecvBuffer, mBufFillSize);
            mBufFillSize = 0;
          }
          break;
        case e_Disconnected:
        case e_Listening:
        case e_ConnectedAndListening:
        default: break;
      }
    }
    return mInterruptResp;
  }

  EComResponse CIPComLayer::recvData(const void *paData, unsigned int) {
    mInterruptResp = e_Nothing;
    switch (mConnectionState) {
      case e_Listening:
        // TODO move this to the processInterrupt()
        mSocketID = arch::CIPComSocketHandler::acceptTCPConnection(mListeningID);
        if (arch::CIPComSocketHandler::scmInvalidSocketDescriptor != mSocketID) {
          DEVLOG_INFO("Connection established by client\n");
          getExtEvHandler<arch::CIPComSocketHandler>().addComCallback(mSocketID, this);
          mConnectionState = e_Connected;
        }
        break;
      case e_Connected:
        if (mSocketID == *(static_cast<const arch::CIPComSocketHandler::TSocketDescriptor *>(paData))) {
          handledConnectedDataRecv();
        } else if (mListeningID == *(static_cast<const arch::CIPComSocketHandler::TSocketDescriptor *>(paData))) {
          handleConnectionAttemptInConnected();
        }
        break;
      case e_ConnectedAndListening:
      case e_Disconnected:
      default: break;
    }
    return mInterruptResp;
  }

  EComResponse CIPComLayer::openConnection(char *paLayerParameter) {
    EComResponse eRetVal = e_InitInvalidId;
    const char *acInterface = arch::CIPComSocketHandler::scmAllInterfaces;
    char *interfaceDest = strchr(paLayerParameter, '@');
    if (interfaceDest) {
      *interfaceDest = '\0';
      acInterface = interfaceDest + 1;
    }

    char *acPort = strchr(paLayerParameter, ':');
    if (nullptr != acPort) {
      *acPort = '\0';
      ++acPort;

      TForteUInt16 nPort = static_cast<TForteUInt16>(util::strtoul(acPort, nullptr, 10));

      arch::CIPComSocketHandler::TSocketDescriptor nSockDes = arch::CIPComSocketHandler::scmInvalidSocketDescriptor;
      mConnectionState = e_Connected;

      switch (mFb->getComServiceType()) {
        case e_Server:
          nSockDes = mListeningID = arch::CIPComSocketHandler::openTCPServerConnection(paLayerParameter, nPort);
          mConnectionState = e_Listening;
          break;
        case e_Client:
          nSockDes = mSocketID = arch::CIPComSocketHandler::openTCPClientConnection(paLayerParameter, nPort);
          break;
        case e_Publisher:
          nSockDes = mSocketID =
              arch::CIPComSocketHandler::openUDPSendPort(paLayerParameter, nPort, &mDestAddr, acInterface);
          break;
        case e_Subscriber:
          nSockDes = mSocketID = arch::CIPComSocketHandler::openUDPReceivePort(paLayerParameter, nPort, acInterface);
          break;
      }

      if (arch::CIPComSocketHandler::scmInvalidSocketDescriptor != nSockDes) {
        if (e_Publisher != mFb->getComServiceType()) {
          // Publishers should not be registered for receiving data
          getExtEvHandler<arch::CIPComSocketHandler>().addComCallback(nSockDes, this);
        }
        eRetVal = e_InitOk;
      } else {
        mConnectionState = e_Disconnected;
      }
    }
    return eRetVal;
  }

  void CIPComLayer::closeConnection() {
    DEVLOG_DEBUG("CSocketBaseLayer::closeConnection() \n");
    closeSocket(&mSocketID);
    closeSocket(&mListeningID);

    mConnectionState = e_Disconnected;
  }

  void CIPComLayer::closeSocket(arch::CIPComSocketHandler::TSocketDescriptor *paSocketID) {
    if (arch::CIPComSocketHandler::scmInvalidSocketDescriptor != *paSocketID) {
      getExtEvHandler<arch::CIPComSocketHandler>().removeComCallback(*paSocketID);
      arch::CIPComSocketHandler::closeSocket(*paSocketID);
      *paSocketID = arch::CIPComSocketHandler::scmInvalidSocketDescriptor;
    }
  }

  void CIPComLayer::handledConnectedDataRecv() {
    // in case of fragmented packets, it can occur that the buffer is full,
    // to avoid calling receiveDataFromTCP with a buffer size of 0 wait until buffer is larger 0
    while ((cgIPLayerRecvBufferSize - mBufFillSize) <= 0) {
      arch::CThread::sleepThread(0);
    }
    if (arch::CIPComSocketHandler::scmInvalidSocketDescriptor != mSocketID) {
      util::CCriticalRegion criticalRegion(mFb->getFBLock());
      int nRetVal = 0;
      switch (mFb->getComServiceType()) {
        case e_Server:
        case e_Client:
          nRetVal = arch::CIPComSocketHandler::receiveDataFromTCP(mSocketID, &mRecvBuffer[mBufFillSize],
                                                                  cgIPLayerRecvBufferSize - mBufFillSize);
          break;
        case e_Publisher:
          // do nothing as publishers cannot receive data
          break;
        case e_Subscriber:
          nRetVal = arch::CIPComSocketHandler::receiveDataFromUDP(mSocketID, &mRecvBuffer[mBufFillSize],
                                                                  cgIPLayerRecvBufferSize - mBufFillSize);
          break;
      }
      switch (nRetVal) {
        case 0:
          DEVLOG_INFO("Connection closed by peer\n");
          mInterruptResp = e_InitTerminated;
          closeSocket(&mSocketID);
          if (e_Server == mFb->getComServiceType()) {
            // Move server into listening mode again
            mConnectionState = e_Listening;
          }
          break;
        case -1: mInterruptResp = e_ProcessDataRecvFaild; break;
        default:
          // we successfully received data
          mBufFillSize += nRetVal;
          mInterruptResp = e_ProcessDataOk;
          break;
      }
      mFb->interruptCommFB(this);
    }
  }

  void CIPComLayer::handleConnectionAttemptInConnected() const {
    // accept and immediately close the connection to tell the client that we are not available
    // so far the best option I've found for handling single connection servers
    arch::CIPComSocketHandler::TSocketDescriptor socketID =
        arch::CIPComSocketHandler::acceptTCPConnection(mListeningID);
    if (arch::CIPComSocketHandler::scmInvalidSocketDescriptor != socketID) {
      arch::CIPComSocketHandler::closeSocket(socketID);
    }
  }
} // namespace forte::com_infra
